package cn.sciento.starter.lock.autoconfigurer;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import cn.sciento.starter.lock.LockAspectHandler;
import cn.sciento.starter.lock.config.LockConfigProperties;
import cn.sciento.starter.lock.factory.LockServiceFactory;
import cn.sciento.starter.lock.factory.ServerPatternFactory;
import cn.sciento.starter.lock.service.impl.FairLockServiceImpl;
import cn.sciento.starter.lock.service.impl.MultiLockServiceImpl;
import cn.sciento.starter.lock.service.impl.ReadLockServiceImpl;
import cn.sciento.starter.lock.service.impl.RedLockServiceImpl;
import org.apache.commons.lang3.StringUtils;
import cn.sciento.starter.lock.LockInfoProvider;
import cn.sciento.starter.lock.enums.ServerPattern;
import cn.sciento.starter.lock.service.impl.ReentrantLockServiceImpl;
import cn.sciento.starter.lock.service.impl.WriteLockServiceImpl;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.ClusterServersConfig;
import org.redisson.config.Config;
import org.redisson.config.MasterSlaveServersConfig;
import org.redisson.config.ReadMode;
import org.redisson.config.ReplicatedServersConfig;
import org.redisson.config.SentinelServersConfig;
import org.redisson.config.SingleServerConfig;
import org.redisson.config.SslProvider;
import org.redisson.config.SubscriptionMode;
import org.redisson.connection.balancer.LoadBalancer;
import org.redisson.connection.balancer.RandomLoadBalancer;
import org.redisson.connection.balancer.RoundRobinLoadBalancer;
import org.redisson.connection.balancer.WeightedRoundRobinBalancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.util.Assert;

@Configuration
@AutoConfigureAfter({RedisAutoConfiguration.class})
@EnableConfigurationProperties({LockConfigProperties.class})
@Import({LockAspectHandler.class})
public class LockAutoConfiguration {
  @Autowired
  private LockConfigProperties lockConfig;
  
  @Bean(name = {"lockRedissonClient"}, destroyMethod = "shutdown")
  @ConditionalOnMissingBean
  RedissonClient redisson() throws Exception {
    SingleServerConfig singleServerConfig;
    ClusterServersConfig clusterConfig;
    MasterSlaveServersConfig masterSlaveConfig;
    ReplicatedServersConfig replicatedServersConfig;
    SentinelServersConfig sentinelServersConfig;
    Config config = new Config();
    config.setThreads(this.lockConfig.getThreads());
    config.setNettyThreads(this.lockConfig.getNettyThreads());
    config.setLockWatchdogTimeout(this.lockConfig.getLockWatchdogTimeout());
    config.setKeepPubSubOrder(this.lockConfig.getKeepPubSubOrder());
    config.setUseScriptCache(this.lockConfig.getUseScriptCache());
    ServerPattern serverPattern = ServerPatternFactory.getServerPattern(this.lockConfig.getPattern());
    switch (serverPattern) {
      case SINGLE:
        singleServerConfig = config.useSingleServer();
        initSingleConfig(singleServerConfig);
        break;
      case CLUSTER:
        clusterConfig = config.useClusterServers();
        initClusterConfig(clusterConfig);
        break;
      case MASTER_SLAVE:
        masterSlaveConfig = config.useMasterSlaveServers();
        initMasterSlaveConfig(masterSlaveConfig);
        break;
      case REPLICATED:
        replicatedServersConfig = config.useReplicatedServers();
        initReplicatedServersConfig(replicatedServersConfig);
        break;
      case SENTINEL:
        sentinelServersConfig = config.useSentinelServers();
        initSentinelServersConfig(sentinelServersConfig);
        break;
    } 
    return Redisson.create(config);
  }
  
  @Bean
  public LockInfoProvider lockInfoProvider() {
    return new LockInfoProvider();
  }
  
  @Bean
  public LockServiceFactory lockFactory() {
    return new LockServiceFactory();
  }
  
  @Bean
  @Scope("prototype")
  public ReentrantLockServiceImpl reentrantLockServiceImpl() {
    return new ReentrantLockServiceImpl();
  }
  
  @Bean
  @Scope("prototype")
  public FairLockServiceImpl fairLockServiceImpl() {
    return new FairLockServiceImpl();
  }
  
  @Bean
  @Scope("prototype")
  public ReadLockServiceImpl readLockServiceImpl() {
    return new ReadLockServiceImpl();
  }
  
  @Bean
  @Scope("prototype")
  public WriteLockServiceImpl writeLockServiceImpl() {
    return new WriteLockServiceImpl();
  }
  
  @Bean
  @Scope("prototype")
  public MultiLockServiceImpl multiLockService() {
    return new MultiLockServiceImpl();
  }
  
  @Bean
  @Scope("prototype")
  public RedLockServiceImpl redLockService() {
    return new RedLockServiceImpl();
  }
  
  private void initSingleConfig(SingleServerConfig singleServerConfig) throws URISyntaxException {
    LockConfigProperties.SingleConfig singleConfig = this.lockConfig.getSingleServer();
    singleServerConfig.setAddress(String.format("%s%s%s%s", new Object[] { "redis://", singleConfig
            .getAddress(), ":", Integer.valueOf(singleConfig.getPort()) }));
    singleServerConfig.setClientName(this.lockConfig.getClientName());
    singleServerConfig.setConnectionMinimumIdleSize(singleConfig.getConnMinIdleSize());
    singleServerConfig.setConnectionPoolSize(singleConfig.getConnPoolSize());
    singleServerConfig.setConnectTimeout(singleConfig.getConnTimeout());
    singleServerConfig.setDatabase(singleConfig.getDatabase());
    singleServerConfig.setDnsMonitoringInterval(singleConfig.getDnsMonitoringInterval());
    singleServerConfig.setIdleConnectionTimeout(singleConfig.getIdleConnTimeout());
    singleServerConfig.setKeepAlive(singleConfig.isKeepAlive());
    if (StringUtils.isNotBlank(singleConfig.getPassword()))
      singleServerConfig.setPassword(singleConfig.getPassword()); 
    singleServerConfig.setRetryAttempts(singleConfig.getRetryAttempts());
    singleServerConfig.setRetryInterval(singleConfig.getRetryInterval());
    singleServerConfig.setSslEnableEndpointIdentification(this.lockConfig.isSslEnableEndpointIdentification());
    if (this.lockConfig.getSslKeystore() != null)
      singleServerConfig.setSslKeystore(new URI(this.lockConfig.getSslKeystore())); 
    if (this.lockConfig.getSslKeystorePassword() != null)
      singleServerConfig.setSslKeystorePassword(this.lockConfig.getSslKeystorePassword()); 
    singleServerConfig
      .setSslProvider("JDK".equalsIgnoreCase(this.lockConfig.getSslProvider()) ? SslProvider.JDK : SslProvider.OPENSSL);
  }
  
  private void initClusterConfig(ClusterServersConfig clusterServerConfig) {
    LockConfigProperties.ClusterConfig clusterConfig = this.lockConfig.getClusterServer();
    String[] addressArr = clusterConfig.getNodeAddresses().split(",");
    Arrays.<String>asList(addressArr).forEach(address -> clusterServerConfig.addNodeAddress(new String[] { String.format("%s%s", new Object[] { "redis://", address }) }));
    clusterServerConfig.setScanInterval(clusterConfig.getScanInterval());
    ReadMode readMode = getReadMode(clusterConfig.getReadMode());
    Assert.notNull(readMode, "Unknown load balancing mode type for read operations");
    clusterServerConfig.setReadMode(readMode);
    SubscriptionMode subscriptionMode = getSubscriptionMode(clusterConfig.getSubMode());
    Assert.notNull(subscriptionMode, "The type of load balancing pattern for an unknown subscription operation");
    clusterServerConfig.setSubscriptionMode(subscriptionMode);
    LoadBalancer loadBalancer = getLoadBalancer(clusterConfig.getLoadBalancer(), clusterConfig.getWeightMaps(), clusterConfig
        .getDefaultWeight());
    Assert.notNull(loadBalancer, "Unknown type of load balancing algorithm");
    clusterServerConfig.setLoadBalancer(loadBalancer);
    clusterServerConfig.setSubscriptionConnectionMinimumIdleSize(clusterConfig.getSubConnMinIdleSize());
    clusterServerConfig.setSubscriptionConnectionPoolSize(clusterConfig.getSubConnPoolSize());
    clusterServerConfig.setSlaveConnectionMinimumIdleSize(clusterConfig.getSlaveConnMinIdleSize());
    clusterServerConfig.setSlaveConnectionPoolSize(clusterConfig.getSlaveConnPoolSize());
    clusterServerConfig.setMasterConnectionMinimumIdleSize(clusterConfig.getMasterConnMinIdleSize());
    clusterServerConfig.setMasterConnectionPoolSize(clusterConfig.getMasterConnPoolSize());
    clusterServerConfig.setIdleConnectionTimeout(clusterConfig.getIdleConnTimeout());
    clusterServerConfig.setConnectTimeout(clusterConfig.getConnTimeout());
    clusterServerConfig.setTimeout(clusterConfig.getTimeout());
    clusterServerConfig.setRetryAttempts(clusterConfig.getRetryAttempts());
    clusterServerConfig.setRetryInterval(clusterConfig.getRetryInterval());
    if (StringUtils.isNotBlank(clusterConfig.getPassword()))
      clusterServerConfig.setPassword(clusterConfig.getPassword()); 
    clusterServerConfig.setSubscriptionsPerConnection(clusterConfig.getSubPerConn());
    clusterServerConfig.setClientName(this.lockConfig.getClientName());
  }
  
  private void initSentinelServersConfig(SentinelServersConfig sentinelServersConfig) throws URISyntaxException {
    LockConfigProperties.SentinelConfig sentinelConfig = this.lockConfig.getSentinelServer();
    String[] addressArr = sentinelConfig.getSentinelAddresses().split(",");
    Arrays.<String>asList(addressArr).forEach(address -> sentinelServersConfig.addSentinelAddress(new String[] { String.format("%s%s", new Object[] { "redis://", address }) }));
    ReadMode readMode = getReadMode(sentinelConfig.getReadMode());
    Assert.notNull(readMode, "Unknown load balancing mode type for read operations");
    sentinelServersConfig.setReadMode(readMode);
    SubscriptionMode subscriptionMode = getSubscriptionMode(sentinelConfig.getSubMode());
    Assert.notNull(subscriptionMode, "The type of load balancing pattern for an unknown subscription operation");
    sentinelServersConfig.setSubscriptionMode(subscriptionMode);
    LoadBalancer loadBalancer = getLoadBalancer(sentinelConfig.getLoadBalancer(), sentinelConfig.getWeightMaps(), sentinelConfig
        .getDefaultWeight());
    Assert.notNull(loadBalancer, "Unknown type of load balancing algorithm");
    sentinelServersConfig.setLoadBalancer(loadBalancer);
    sentinelServersConfig.setMasterName(sentinelConfig.getMasterName());
    sentinelServersConfig.setDatabase(sentinelConfig.getDatabase());
    sentinelServersConfig.setSlaveConnectionPoolSize(sentinelConfig.getSlaveConnectionPoolSize());
    sentinelServersConfig.setMasterConnectionPoolSize(sentinelConfig.getMasterConnectionPoolSize());
    sentinelServersConfig.setSubscriptionConnectionPoolSize(sentinelConfig.getSubscriptionConnectionPoolSize());
    sentinelServersConfig.setSlaveConnectionMinimumIdleSize(sentinelConfig.getSlaveConnectionMinimumIdleSize());
    sentinelServersConfig.setMasterConnectionMinimumIdleSize(sentinelConfig.getMasterConnectionMinimumIdleSize());
    sentinelServersConfig.setSubscriptionConnectionMinimumIdleSize(sentinelConfig.getSubscriptionConnectionMinimumIdleSize());
    sentinelServersConfig.setDnsMonitoringInterval(sentinelConfig.getDnsMonitoringInterval());
    sentinelServersConfig.setSubscriptionsPerConnection(sentinelConfig.getSubscriptionsPerConnection());
    if (StringUtils.isNotBlank(sentinelConfig.getPassword()))
      sentinelServersConfig.setPassword(sentinelConfig.getPassword()); 
    sentinelServersConfig.setRetryAttempts(sentinelConfig.getRetryAttempts());
    sentinelServersConfig.setRetryInterval(sentinelConfig.getRetryInterval());
    sentinelServersConfig.setTimeout(sentinelConfig.getTimeout());
    sentinelServersConfig.setConnectTimeout(sentinelConfig.getConnectTimeout());
    sentinelServersConfig.setIdleConnectionTimeout(sentinelConfig.getIdleConnectionTimeout());
    setLockSslConfigAndClientName(sentinelServersConfig);
  }
  
  private void initReplicatedServersConfig(ReplicatedServersConfig replicatedServersConfig) throws URISyntaxException {
    LockConfigProperties.ReplicatedConfig replicatedConfig = this.lockConfig.getReplicatedServer();
    String[] addressArr = replicatedConfig.getNodeAddresses().split(",");
    Arrays.<String>asList(addressArr).forEach(address -> replicatedServersConfig.addNodeAddress(new String[] { String.format("%s%s", new Object[] { "redis://", address }) }));
    ReadMode readMode = getReadMode(replicatedConfig.getReadMode());
    Assert.notNull(readMode, "Unknown load balancing mode type for read operations");
    replicatedServersConfig.setReadMode(readMode);
    SubscriptionMode subscriptionMode = getSubscriptionMode(replicatedConfig.getSubscriptionMode());
    Assert.notNull(subscriptionMode, "The type of load balancing pattern for an unknown subscription operation");
    replicatedServersConfig.setSubscriptionMode(subscriptionMode);
    LoadBalancer loadBalancer = getLoadBalancer(replicatedConfig.getLoadBalancer(), replicatedConfig
        .getWeightMaps(), replicatedConfig.getDefaultWeight());
    Assert.notNull(loadBalancer, "Unknown type of load balancing algorithm");
    replicatedServersConfig.setLoadBalancer(loadBalancer);
    replicatedServersConfig.setScanInterval(replicatedConfig.getScanInterval());
    replicatedServersConfig.setDatabase(replicatedConfig.getDatabase());
    replicatedServersConfig.setSlaveConnectionPoolSize(replicatedConfig.getSlaveConnectionPoolSize());
    replicatedServersConfig.setMasterConnectionPoolSize(replicatedConfig.getMasterConnectionPoolSize());
    replicatedServersConfig.setSubscriptionConnectionPoolSize(replicatedConfig.getSubscriptionConnectionPoolSize());
    replicatedServersConfig.setSlaveConnectionMinimumIdleSize(replicatedConfig.getSlaveConnectionMinimumIdleSize());
    replicatedServersConfig.setMasterConnectionMinimumIdleSize(replicatedConfig.getMasterConnectionMinimumIdleSize());
    replicatedServersConfig.setSubscriptionConnectionMinimumIdleSize(replicatedConfig.getSubscriptionConnectionMinimumIdleSize());
    replicatedServersConfig.setDnsMonitoringInterval(replicatedConfig.getDnsMonitoringInterval());
    replicatedServersConfig.setSubscriptionsPerConnection(replicatedConfig.getSubscriptionsPerConnection());
    if (StringUtils.isNotBlank(replicatedConfig.getPassword()))
      replicatedServersConfig.setPassword(replicatedConfig.getPassword()); 
    replicatedServersConfig.setRetryAttempts(replicatedConfig.getRetryAttempts());
    replicatedServersConfig.setRetryInterval(replicatedConfig.getRetryInterval());
    replicatedServersConfig.setTimeout(replicatedConfig.getTimeout());
    replicatedServersConfig.setConnectTimeout(replicatedConfig.getConnectTimeout());
    replicatedServersConfig.setIdleConnectionTimeout(replicatedConfig.getIdleConnectionTimeout());
    setLockSslConfigAndClientName(replicatedServersConfig);
  }
  
  private void initMasterSlaveConfig(MasterSlaveServersConfig masterSlaveServersConfig) throws URISyntaxException {
    LockConfigProperties.MasterSlaveConfig masterSlaveConfig = this.lockConfig.getMasterSlaveServer();
    masterSlaveServersConfig.setMasterAddress(
        String.format("%s%s", new Object[] { "redis://", masterSlaveConfig.getMasterAddress() }));
    String[] addressArr = masterSlaveConfig.getSlaveAddresses().split(",");
    Arrays.<String>asList(addressArr).forEach(address -> masterSlaveServersConfig.addSlaveAddress(new String[] { String.format("%s%s", new Object[] { "redis://", address }) }));
    ReadMode readMode = getReadMode(masterSlaveConfig.getReadMode());
    Assert.notNull(readMode, "Unknown load balancing mode type for read operations");
    masterSlaveServersConfig.setReadMode(readMode);
    SubscriptionMode subscriptionMode = getSubscriptionMode(masterSlaveConfig.getSubMode());
    Assert.notNull(subscriptionMode, "The type of load balancing pattern for an unknown subscription operation");
    masterSlaveServersConfig.setSubscriptionMode(subscriptionMode);
    LoadBalancer loadBalancer = getLoadBalancer(masterSlaveConfig.getLoadBalancer(), masterSlaveConfig
        .getWeightMaps(), masterSlaveConfig.getDefaultWeight());
    Assert.notNull(loadBalancer, "Unknown type of load balancing algorithm");
    masterSlaveServersConfig.setLoadBalancer(loadBalancer);
    masterSlaveServersConfig.setDatabase(masterSlaveConfig.getDatabase());
    masterSlaveServersConfig.setSlaveConnectionPoolSize(masterSlaveConfig.getSlaveConnectionPoolSize());
    masterSlaveServersConfig.setMasterConnectionPoolSize(masterSlaveConfig.getMasterConnectionPoolSize());
    masterSlaveServersConfig.setSubscriptionConnectionPoolSize(masterSlaveConfig.getSubscriptionConnectionPoolSize());
    masterSlaveServersConfig.setSlaveConnectionMinimumIdleSize(masterSlaveConfig.getSlaveConnectionMinimumIdleSize());
    masterSlaveServersConfig.setMasterConnectionMinimumIdleSize(masterSlaveConfig.getMasterConnectionMinimumIdleSize());
    masterSlaveServersConfig.setSubscriptionConnectionMinimumIdleSize(masterSlaveConfig.getSubscriptionConnectionMinimumIdleSize());
    masterSlaveServersConfig.setDnsMonitoringInterval(masterSlaveConfig.getDnsMonitoringInterval());
    masterSlaveServersConfig.setSubscriptionsPerConnection(masterSlaveConfig.getSubscriptionsPerConnection());
    if (StringUtils.isNotBlank(masterSlaveConfig.getPassword()))
      masterSlaveServersConfig.setPassword(masterSlaveConfig.getPassword()); 
    masterSlaveServersConfig.setRetryAttempts(masterSlaveConfig.getRetryAttempts());
    masterSlaveServersConfig.setRetryInterval(masterSlaveConfig.getRetryInterval());
    masterSlaveServersConfig.setTimeout(masterSlaveConfig.getTimeout());
    masterSlaveServersConfig.setConnectTimeout(masterSlaveConfig.getConnectTimeout());
    masterSlaveServersConfig.setIdleConnectionTimeout(masterSlaveConfig.getIdleConnectionTimeout());
    setLockSslConfigAndClientName(masterSlaveServersConfig);
  }
  
  private LoadBalancer getLoadBalancer(String loadBalancerType, String customerWeightMaps, int defaultWeight) {
    if ("RandomLoadBalancer".equals(loadBalancerType))
      return (LoadBalancer)new RandomLoadBalancer(); 
    if ("RoundRobinLoadBalancer".equals(loadBalancerType))
      return (LoadBalancer)new RoundRobinLoadBalancer(); 
    if ("WeightedRoundRobinBalancer".equals(loadBalancerType)) {
      Map<String, Integer> weights = new HashMap<>(16);
      String[] weightMaps = customerWeightMaps.split(";");
      Arrays.<String>asList(weightMaps)
        .forEach(weightMap ->weights.put("redis://" + weightMap.split(",")[0], Integer.parseInt(weightMap.split(",")[1])));
      return (LoadBalancer)new WeightedRoundRobinBalancer(weights, defaultWeight);
    } 
    return null;
  }
  
  private ReadMode getReadMode(String readModeType) {
    if ("SLAVE".equals(readModeType))
      return ReadMode.SLAVE; 
    if ("MASTER".equals(readModeType))
      return ReadMode.MASTER; 
    if ("MASTER_SLAVE".equals(readModeType))
      return ReadMode.MASTER_SLAVE; 
    return null;
  }
  
  private SubscriptionMode getSubscriptionMode(String subscriptionModeType) {
    if ("SLAVE".equals(subscriptionModeType))
      return SubscriptionMode.SLAVE; 
    if ("MASTER".equals(subscriptionModeType))
      return SubscriptionMode.MASTER; 
    return null;
  }
  
  private <T extends org.redisson.config.BaseMasterSlaveServersConfig> void setLockSslConfigAndClientName(T lockAutoConfig) throws URISyntaxException {
    if (this.lockConfig.isSslEnableEndpointIdentification()) {
      lockAutoConfig.setClientName(this.lockConfig.getClientName());
      lockAutoConfig.setSslEnableEndpointIdentification(this.lockConfig.isSslEnableEndpointIdentification());
      if (this.lockConfig.getSslKeystore() != null)
        lockAutoConfig.setSslKeystore(new URI(this.lockConfig.getSslKeystore())); 
      if (this.lockConfig.getSslKeystorePassword() != null)
        lockAutoConfig.setSslKeystorePassword(this.lockConfig.getSslKeystorePassword()); 
      if (this.lockConfig.getSslTruststore() != null)
        lockAutoConfig.setSslTruststore(new URI(this.lockConfig.getSslTruststore())); 
      if (this.lockConfig.getSslTruststorePassword() != null)
        lockAutoConfig.setSslTruststorePassword(this.lockConfig.getSslTruststorePassword()); 
      lockAutoConfig.setSslProvider("JDK".equalsIgnoreCase(this.lockConfig.getSslProvider()) ? SslProvider.JDK : SslProvider.OPENSSL);
    } 
  }
}
